<template>
    <div class="demographic-3D-container">
        <div id="demographic-3D-ui"></div>
    </div>
</template>

<script>
import { MeshBasicMaterial, SceneManager, SceneParams, createBoxGeometry, createBufferAttribute, createColor, createMaterial, createMesh, createObject3D, createSphereGeometry, degToRad, lerp, mergeGeometry } from '@/components/common/scenemanager';
import ErrorEmote from '@/components/specialeffects/ErrorEmote.vue';
import { h, render } from 'vue';

export default {
    name: "Demographic3D",
    components: {},
    data() {
        return {
            hasError: false,
            sceneManager: null,
        }
    },
    computed: {},
    methods: {
        //加载失败
        loadError(error) {
            console.error(error);
            this.hasError = true;
            const view = this.$el.parentElement.querySelector(".demographic-3D-container");
            const errorIns = h(ErrorEmote);
            render(errorIns, view);
        },
        //加载人口统计数据文件
        async loadFile(url) {
            const res = await fetch(url);
            return res.text();
        },
        async loadData(info) {
            const text = await this.loadFile(info.url);
            info.file = this.parseData(text);
        },

        async loadAll() {
            const fileInfos = [
                { name: 'men', hueRange: [0.7, 0.3], url: '/data/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
                { name: 'women', hueRange: [0.9, 1.1], url: '/data/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
            ];

            await Promise.all(fileInfos.map(this.loadData));

            const menInfo = fileInfos[0];
            const womenInfo = fileInfos[1];
            const menFile = menInfo.file;
            const womenFile = womenInfo.file;

            fileInfos.push({
                name: '>50%men',
                hueRange: [0.6, 1.1],
                file: this.makeDiffFile(menFile, womenFile, (men, women) => {
                    return this.amountGreaterThan(men, women);
                }),
            });
            fileInfos.push({
                name: '>50% women',
                hueRange: [0.0, 0.4],
                file: this.makeDiffFile(womenFile, menFile, (women, men) => {
                    return this.amountGreaterThan(women, men);
                }),
            });

            // 对每一个数据集生成几何体
            const geometries = fileInfos.map((info) => {
                return this.makeBoxes(info.file, info.hueRange, fileInfos);
            });

            // 以第一个几何体作为基准, 将其他的作为变形目标
            const baseGeometry = geometries[0];
            baseGeometry.morphAttributes.position = geometries.map((geometry, ndx) => {
                const attribute = geometry.getAttribute('position');
                const name = `target${ndx}`;
                attribute.name = name;
                return attribute;
            });
            baseGeometry.morphAttributes.color = geometries.map((geometry, ndx) => {
                const attribute = geometry.getAttribute('color');
                const name = `target${ndx}`;
                attribute.name = name;
                return attribute;
            });

            const material = createMaterial({
                vertexColors: true,
            }, MeshBasicMaterial);
            const mesh = createMesh("grid-data", baseGeometry, material);

            this.sceneManager.addModelToScene(0, mesh);

            const uiElem = document.querySelector('#demographic-3D-ui');
            fileInfos.forEach((info) => {
                const div = document.createElement('div');
                info.elem = div;
                div.textContent = info.name;
                uiElem.appendChild(div);
                div.addEventListener('mouseover', () => {
                    this.showFileInfo(fileInfos, info, mesh);
                });
            });

            // 起始展示第一组数据
            this.showFileInfo(fileInfos, fileInfos[0], mesh);
            this.draw();
        },
        // 展示选中的元素, 隐藏其他的
        showFileInfo(fileInfos, fileInfo, mesh) {
            fileInfos.forEach((info) => {
                const visible = fileInfo === info;
                info.elem.className = visible ? 'selected' : '';

                const targets = {};
                fileInfos.forEach((info, i) => {
                    targets[i] = info === fileInfo ? 1 : 0;
                });
                const durationInMs = 1000;
                this.sceneManager.getTweenManger().createTween(mesh.morphTargetInfluences)
                    .to(targets, durationInMs)
                    .start();
            });
        },

        amountGreaterThan(a, b) {
            return Math.max(a - b, 0);
        },
        parseData(text) {
            if (!text) {
                return;
            }
            const data = [];
            const settings = { data };
            let max;
            let min;
            // 对每一行进行切分
            text.split('\n').forEach((line) => {
                // split the line by whitespace
                const parts = line.trim().split(/\s+/);
                if (parts.length === 2) {
                    // 长度为2的必定是键值对
                    settings[parts[0]] = parseFloat(parts[1]);
                } else if (parts.length > 2) {
                    // 长度超过2的肯定是网格数据
                    const values = parts.map((v) => {
                        const value = parseFloat(v);
                        if (value === settings.NODATA_value) {
                            return undefined;
                        }
                        max = Math.max(max === undefined ? value : max, value);
                        min = Math.min(min === undefined ? value : min, value);
                        return value;
                    });
                    data.push(values);
                }
            });
            return Object.assign(settings, { min, max });
        },
        dataMissingInAnySet(fileInfos, latNdx, lonNdx) {
            for (const fileInfo of fileInfos) {
                if (fileInfo.file.data[latNdx][lonNdx] === undefined) {
                    return true;
                }
            }
            return false;
        },
        makeBoxes(file, hueRange, fileInfos) {
            if (!file) {
                this.loadError("格式错误");
                return;
            }
            const { min, max, data } = file;
            const range = max - min;

            // 位置辅助器可以方便地在球面上定位
            // 经度辅助器可以在XZ平面的法向旋转
            const lonHelper = createObject3D();
            this.sceneManager.addModelToScene(0, lonHelper);
            // 纬度辅助器可以在XZ平面旋转
            const latHelper = createObject3D();
            lonHelper.add(latHelper);
            // 组合起来得到的位置辅助器可以在球面上定位
            const positionHelper = createObject3D();
            positionHelper.position.z = 1;
            latHelper.add(positionHelper);

            // 用来定位盒子的中心, 以便接下来沿着Z轴缩放
            const originHelper = createObject3D();
            originHelper.position.z = 0.5;
            positionHelper.add(originHelper);

            const color = createColor();
            const lonFudge = Math.PI * .5;
            const latFudge = Math.PI * -0.135;
            const geometries = [];
            data.forEach((row, latNdx) => {
                row.forEach((value, lonNdx) => {
                    if (value === undefined) {
                        return;
                    }
                    if (this.dataMissingInAnySet(fileInfos, latNdx, lonNdx)) {
                        return;
                    }
                    const amount = (value - min) / range;
                    const boxWidth = 1;
                    const boxHeight = 1;
                    const boxDepth = 1;
                    const geometry = createBoxGeometry(boxWidth, boxHeight, boxDepth);

                    // 调整辅助器使其指向经纬度
                    lonHelper.rotation.y = degToRad(lonNdx + file.xllcorner) + lonFudge;
                    latHelper.rotation.x = degToRad(latNdx + file.yllcorner) + latFudge;

                    // 使用位置辅助器和world matrix 来定位
                    positionHelper.scale.set(0.005, 0.005, lerp(0.01, 0.5, amount));
                    originHelper.updateWorldMatrix(true, false);
                    geometry.applyMatrix4(originHelper.matrixWorld);

                    // 计算颜色
                    const hue = lerp(...hueRange, amount);
                    const saturation = 1;
                    const lightness = lerp(0.4, 1.0, amount);
                    color.setHSL(hue, saturation, lightness);
                    // 以0到255之间的值数组形式获取颜色
                    const rgb = color.toArray().map(v => v * 255);

                    // 创建一个数组来存储每个顶点的颜色
                    const numVerts = geometry.getAttribute('position').count;
                    const itemSize = 3;  // r, g, b
                    const colors = new Uint8Array(itemSize * numVerts);

                    // 将颜色复制到每个顶点的颜色数组中
                    colors.forEach((v, ndx) => {
                        colors[ndx] = rgb[ndx % 3];
                    });

                    const normalized = true;
                    const colorAttrib = createBufferAttribute(colors, itemSize, normalized);
                    geometry.setAttribute('color', colorAttrib);
                    geometries.push(geometry);
                });
            });
            return mergeGeometry(geometries);
        },
        createEarth() {
            this.sceneManager.loadHDRAndTX([], [
                "/img/specialeffects/world.jpg",
            ], () => {
                const geometry = createSphereGeometry(1, 64, 32);
                const earth = createMesh("earth", geometry);

                const earthTX = this.sceneManager.txMap.get("/img/specialeffects/world.jpg");
                this.sceneManager.addMaterial("earth", { map: earthTX }, MeshBasicMaterial);

                this.sceneManager.addMaterialForModel(earth);
                this.sceneManager.addModelToScene(0, earth);
            }, undefined, this.loadError);
        },
        loadModel() {
            this.createEarth();
            this.loadAll();
        },
        mapValues(data, fn) {
            return data.map((row, rowNdx) => {
                return row.map((value, colNdx) => {
                    return fn(value, rowNdx, colNdx);
                });
            });
        },
        makeDiffFile(baseFile, otherFile, compareFn) {
            let min;
            let max;
            const baseData = baseFile.data;
            const otherData = otherFile.data;
            const data = this.mapValues(baseData, (base, rowNdx, colNdx) => {
                const other = otherData[rowNdx][colNdx];
                if (base === undefined || other === undefined) {
                    return undefined;
                }
                const value = compareFn(base, other);
                min = Math.min(min === undefined ? value : min, value);
                max = Math.max(max === undefined ? value : max, value);
                return value;
            });
            // 生成baseFile的一个副本, 然后用新文件的min max 和 data替代原来的
            return { ...baseFile, min, max, data };
        },
        //开始渲染
        draw() {
            if (this.hasError) {
                return;
            }

            const view = this.$el.parentElement.querySelector(".demographic-3D-container");
            view.appendChild(this.sceneManager.getRenderDom());

            this.sceneManager.start();
        },
        //场景参数
        createSceneParams() {
            const params = new SceneParams();

            params.cameraParams.fov = 60;
            params.cameraParams.near = 0.1;
            params.cameraParams.far = 10;
            params.cameraParams.position.x = 0.2;
            params.cameraParams.position.y = 2.5;
            params.cameraParams.position.z = 1.5;

            params.controlsParams.enablePan = false;
            params.controlsParams.enableDamping = true;
            params.controlsParams.autoRotate = true;

            return params;
        },
    },
    mounted() {
        const view = this.$el.parentElement.querySelector(".demographic-3D-container");
        this.sceneManager = new SceneManager(view.offsetWidth, view.offsetHeight, false);
        this.sceneManager.addScene(this.createSceneParams(), undefined);
        this.loadModel();
    },
    beforeUnmount() {
        this.sceneManager.destroy();
    },
}
</script>

<style>
.demographic-3D-container {
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    position: relative;
}

#demographic-3D-ui {
    position: absolute;
    left: 30%;
    bottom: 1em;
}

#demographic-3D-ui>div {
    color: blue;
    font-size: 20pt;
    padding: 1em;
    display: inline-block;
}

#demographic-3D-ui>div.selected {
    color: red;
}
</style>